<?php
/**
 * Library Of Shared Code (LOSC)
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @category   LOSC Framework
 * @package    Auth
 * @subpackage Adapter
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */

/**
 * Auth adapter using Doctrine
 * 
 * @category   LOSC Framework
 * @package    Auth
 * @subpackage Adapter
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */
class Losc_Security_Auth_Adapter_Doctrine implements Zend_Auth_Adapter_Interface
{
    /**
     * Doctrine table name
     *
     * @var string
     */
    protected $_tableName = 'App_User u';

    /**
     * The column to use as the identity
     *
     * @var string
     */
    protected $_identityColumn = 'u.username';

    /**
     * Column to be used as the credential
     *
     * @var string
     */
    protected $_credentialColumn = 'u.password';

    /**
     * Identity value
     *
     * @var string
     */
    protected $_identity = null;

    /**
     * Credential value
     *
     * @var string
     */
    protected $_credential = null;

    /**
     * Treatment applied to the credential, such as MD5() or PASSWORD()
     *
     * @var string
     */
    protected $_credentialTreatment = 'md5(?)';

    /**
     * Result information
     *
     * @var array
     */
    protected $_authResult = null;
    
    /**
     * Results of database authentication query
     *
     * @var array
     */
    protected $_resultRow = null;

    /**
     * Creates auth adapter
     *
     * @param  string $tableName            [optinoal] table name,
     *                                      default is 'App_User u'
     * @param  string $identityColumn       [optional] identity column,
     *                                      default is 'u.username'
     * @param  string $credentialColumn     [optional] credential column,
     *                                      default is 'u.password'
     * @param  string $credentialTreatment  [optional] credential treatment,
     *                                      default is 'md5(?)'
     * @return void
     */
    public function __construct($tableName = null,
                                $identityColumn = null,
                                $credentialColumn = null,
                                $credentialTreatment = null)
    {
        if (null !== $tableName) {
            $this->setTableName($tableName);
        }

        if (null !== $identityColumn) {
            $this->setIdentityColumn($identityColumn);
        }

        if (null !== $credentialColumn) {
            $this->setCredentialColumn($credentialColumn);
        }

        if (null !== $credentialTreatment) {
            $this->setCredentialTreatment($credentialTreatment);
        }
    }

    /**
     * Sets the table name to be used in the select query
     *
     * @param  string $tableName           table name
     * @return Losc_Security_Auth_Adapter_Doctrine  provides a fluent interface
     */
    public function setTableName($tableName)
    {
        $this->_tableName = $tableName;
        return $this;
    }

    /**
     * Sets the column name to be used as the identity column
     *
     * @param  string $identityColumn      identity column
     * @return Losc_Security_Auth_Adapter_Doctrine  provides a fluent interface
     */
    public function setIdentityColumn($identityColumn)
    {
        $this->_identityColumn = $identityColumn;
        return $this;
    }

    /**
     * Specifies that that adapter should use 'u.email' as identity column
     *
     * @return Losc_Security_Auth_Adapter_Doctrine  provides a fluent interface
     */
    public function setUseEmail()
    {
        $this->_identityColumn = 'u.email';
        return $this;
    }

    /**
     * Sets the column name to be used as the credential column
     *
     * @param  string $credentialColumn    credential column
     * @return Losc_Security_Auth_Adapter_Doctrine  provides a fluent interface
     */
    public function setCredentialColumn($credentialColumn)
    {
        $this->_credentialColumn = $credentialColumn;
        return $this;
    }

    /**
     * Allows the developer to pass a parameterized string that is used to 
     * transform or treat the input credential data
     *
     * In many cases, passwords and other sensitive data are encrypted, hashed,
     * encoded, obscured, or otherwise treated through some function or
     * algorithm. By specifying a parameterized treatment string with this 
     * method, a developer may apply arbitrary SQL upon input credential data.
     *
     * Examples:
     *  'PASSWORD(?)'
     *  'MD5(?)'
     *
     * @param  string $treatment           credential treatment
     * @return Losc_Security_Auth_Adapter_Doctrine  provides a fluent interface
     */
    public function setCredentialTreatment($treatment)
    {
        $this->_credentialTreatment = $treatment;
        return $this;
    }

    /**
     * Set the value to be used as the identity
     *
     * @param  string $value               identity value
     * @return Losc_Security_Auth_Adapter_Doctrine  provides a fluent interface
     */
    public function setIdentity($value)
    {
        $this->_identity = $value;
        return $this;
    }

    /**
     * Set the value to be used as credential
     *
     * @param  string $credential          credential value
     * @return Losc_Security_Auth_Adapter_Doctrine  provides a fluent interface
     */
    public function setCredential($credential)
    {
        $this->_credential = $credential;
        return $this;
    }

    /**
     * Defined by Zend_Auth_Adapter_Interface
     * 
     * This method is called to attempt an authentication. Previous to this call,
     * this adapter would have already been configured with all necessary 
     * information to successfully connect to a database table and attempt to 
     * find a record matching the provided identity.
     *
     * @throws Zend_Auth_Adapter_Exception  if answering the authentication 
     *                                      query is impossible
     * @return Zend_Auth_Result
     */
    public function authenticate()
    {
        $this->_authenticateSetup();
        
        $dbSelect = $this->_authenticateCreateQuery();
        
        $resultIdentities = $this->_authenticateExecuteQuery($dbSelect);
        
        $authResult = $this->_authenticateValidateResultset($resultIdentities);
        if ($authResult instanceof Zend_Auth_Result) {
            return $authResult;
        }
        
        $authResult = array_shift($resultIdentities);
        $authResult = $this->_authenticateValidateResult($authResult);
        
        return $authResult;
    }
    
    /**
     * Returns username from result
     * 
     * Returns false if there is no result row.
     *
     * @return int|false  uid or false
     */
    public function getUsername()
    {
        if (!$this->_resultRow) {
            return false;
        }
        
        return $this->_resultRow['username'];
    }

    /**
     * Sets up stuff before authentication
     * 
     * This method abstracts the steps involved with making sure that this 
     * adapter was indeed setup properly with all required peices of 
     * information.
     *
     * @throws Zend_Auth_Adapter_Exception  if the setup was not done properly
     * @return true
     */
    protected function _authenticateSetup()
    {
        $exception = null;

        if ($this->_tableName == '') {
            $exception = 'A table must be supplied for the '
                       . 'Losc_Security_Auth_Adapter_Doctrine authentication adapter';
                       
        } elseif ($this->_identityColumn == '') {
            $exception = 'An identity column must be supplied for the '
                       . 'Losc_Security_Auth_Adapter_Doctrine authentication adapter';
                       
        } elseif ($this->_credentialColumn == '') {
            $exception = 'A credential column must be supplied for the '
                       . 'Losc_Security_Auth_Adapter_Doctrine authentication adapter';
                       
        } elseif ($this->_identity == '') {
            $exception = 'A value for the identity was not provided prior to '
                       . 'authentication with Losc_Security_Auth_Adapter_Doctrine';
                       
        } elseif ($this->_credential === null) {
            $exception = 'A credential value was not provided prior to '
                       . 'authentication with Losc_Security_Auth_Adapter_Doctrine';
        }

        if (null !== $exception) {
            throw new Zend_Auth_Adapter_Exception($exception);
        }
        
        $this->_authResult = array(
            'code'     => Zend_Auth_Result::FAILURE,
            'identity' => $this->_identity,
            'messages' => array());
            
        return true;
    }

    /**
     * Returns a Doctrine_Query object that is completely configured to be 
     * queried against the database
     * 
     * Ensures that the 'confirmation' column is null and the user is not
     * deleted.
     *
     * @return Doctrine_Query  query
     */
    protected function _authenticateCreateQuery()
    {
        // build credential expression
        if (empty($this->_credentialTreatment) ||
            (strpos($this->_credentialTreatment, "?") === false)) {
            $this->_credentialTreatment = '?';
        }
        
        $conn = Doctrine::getConnectionByTableName($this->_tableName);
        
        $query = Doctrine_Query::create($conn)
                 ->from($this->_tableName)
                 ->select("*, ({$this->_credentialColumn} = "
                            . str_replace('?',
                                          $conn->quote($this->_credential),
                                          $this->_credentialTreatment)
                            . ') AS zend_auth_credential_match')
                 ->addWhere($this->_identityColumn .' = ?', $this->_identity)
                 ->addWhere('confirmation is null')
                 ->addWhere('deleted = 0');

        return $query;
    }

    /**
     * This method accepts a Doctrine_Query object and performs a query
     * against the database with that object
     *
     * @param  Doctrine_Query $query        query
     * @throws Zend_Auth_Adapter_Exception  if $query is invalid
     * @return array                        matching row(s)
     */
    protected function _authenticateExecuteQuery(Doctrine_Query $query)
    {
        try {
            $resultIdentities = $query->execute()->toArray();
        } catch (Exception $e) {
            $msg = 'The supplied parameters to Losc_Security_Auth_Adapter_Doctrine '
                 . 'failed to produce a valid sql statement, please check '
                 . 'table and column names for validity';
            throw new Zend_Auth_Adapter_Exception($msg);
        }
        
        return $resultIdentities;
    }

    /**
     * This method attempts to make certian that only one record was returned
     * in the result set
     *
     * @param  array $resultIdentities
     * @return true|Zend_Auth_Result
     */
    protected function _authenticateValidateResultSet(array $resultIdentities)
    {
        if (count($resultIdentities) < 1) {
            $this->_authResult['code'] =
                Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND;
            $this->_authResult['messages'][] =
                'A record with the supplied identity could not be found';
            return $this->_authenticateCreateAuthResult();
        } elseif (count($resultIdentities) > 1) {
            $this->_authResult['code'] =
                Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS;
            $this->_authResult['messages'][] =
                'More than one record matches the supplied identity';
            return $this->_authenticateCreateAuthResult();
        }

        return true;
    }

    /**
     * This method attempts to validate that the record in the result set is
     * indeed a record that matched the identity provided to this adapter
     *
     * @param array $resultIdentity
     * @return Zend_Auth_Result
     */
    protected function _authenticateValidateResult($resultIdentity)
    {
        if ($resultIdentity['zend_auth_credential_match'] != '1') {
            $this->_authResult['code'] =
                Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID;
            $this->_authResult['messages'][] =
                'Supplied credential is invalid';
            return $this->_authenticateCreateAuthResult();
        }

        unset($resultIdentity['zend_auth_credential_match']);
        $this->_resultRow = $resultIdentity;

        $this->_authResult['code'] = Zend_Auth_Result::SUCCESS;
        $this->_authResult['messages'][] = 'Authentication successful.';
        return $this->_authenticateCreateAuthResult();
    }
    
    /**
     * This method creates a Zend_Auth_Result object from the information
     * that has been collected during the authenticate() attempt
     *
     * @return Zend_Auth_Result  auth result
     */
    protected function _authenticateCreateAuthResult()
    {
        return new Zend_Auth_Result($this->_authResult['code'],
                                    $this->_authResult['identity'],
                                    $this->_authResult['messages']);
    }
}
